{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Medoid Radius\n",
    "\n",
    "Medoid Radius is a cluster algorithm that I use in one of my projects. I did not find not cluster algorithm suitable for my case such that I proposed this novel algorithm.\n",
    "\n",
    "My case is like that, each data point is a shape, I managed to find a way to measure the distance matrix for the shapes. Another requirement is that we only cluster data points that are very similar together. Data points without a near neighbor are discarded.\n",
    "\n",
    "\n",
    "## Example\n",
    "Suppose we have the following RNA sequences.\n",
    "\n",
    "![](medoid_radius/rna_123.jpg)\n",
    "![](medoid_radius/rna_456.jpg)\n",
    "\n",
    "We number them from 0 to 5, and the distance matrix is as follow:\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<Figure size 432x288 with 2 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "import numpy as np\n",
    "from matplotlib import pyplot as plt\n",
    "%matplotlib inline\n",
    "import seaborn as sns\n",
    "\n",
    "D=np.array([\n",
    "    [0.0, 0.1, 0.5, 0.5, 0.8, 0.8],\n",
    "    [0.1, 0.0, 0.6, 0.6, 0.8, 0.8],\n",
    "    [0.5, 0.6, 0.0, 0.1, 0.7, 0.7],\n",
    "    [0.5, 0.6, 0.1, 0.0, 0.7, 0.7],\n",
    "    [0.8, 0.8, 0.7, 0.7, 0.0, 0.3],\n",
    "    [0.8, 0.8, 0.7, 0.7, 0.3, 0.0]])\n",
    "\n",
    "sns.heatmap(D)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here, we have two clusters, (0, 1) and (2, 3). 4 and 5 are not similar enough that they can be grouped together, neither are they similar to the rest RNAs. Thus, 4 and 5 are to be discarded."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Steps:\n",
    "\n",
    "Input: Distance Matrix $D$, radius $r$\n",
    "\n",
    "Step 1: Find the largest cluster $C$, with points $(k, l, ...)$. If no cluster is found, quit.\n",
    "  * Scan every row $i$ in $D$, Select a row $i$ with the most satisfied data points($d_{ij}<r$)\n",
    "  * $d_{ij}$ denotes distance between Data Point $i$ and $j$\n",
    "  \n",
    "$$\n",
    "\\underset {i}{\\operatorname{argmax}} \\sum_{j=0}^{n} (d_{ij}<r), (d_{ij}<r\\ =\\ 1\\ if\\ true, 0\\ if\\ false)\n",
    "$$\n",
    "\n",
    "Step 2: Find the medoid, by calculating the summation distance from every data point to other data points in the cluster.\n",
    "  * Find data point $m$ that minimize the summation of distances from point $i$ to other points (denoted as $j$) within the cluster.\n",
    "$$\n",
    "m=\\underset {k \\in C}{ \\operatorname{argmin}} \\sum_{k\\neq l, l \\in C}^{}d_{kl}\n",
    "$$\n",
    "\n",
    "\n",
    "\n",
    "Step 3: Rescan every data point in row $m$ . Data points satisfies $d_{mj}<r$ are selected.\n",
    "\n",
    "Step 4: Repeat Step 2 and 3, until the cluster numbers are fixed. Update labels. \n",
    "  * For $(i, j, k ...)$ in cluster $C$ and medoid $m$, Set $label(i)=m$.\n",
    "\n",
    "Step 5: Goto Step 1."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "class MedoidRadius(object):\n",
    "    distance_matrix_ = None\n",
    "    n_samples=0\n",
    "    labels_ = []\n",
    "    medoids_ = []\n",
    "    \n",
    "    #select\n",
    "    def __select_medoid(self, mask_row):\n",
    "        members=mask_row.argsort()[-mask_row.sum():]\n",
    "        n_members=len(members)\n",
    "        distances=np.zeros((n_members))\n",
    "        index=0\n",
    "        for member in members:\n",
    "            \n",
    "            distance = sum(self.matrix_distances_[member] * mask_row)/sum(mask_row)\n",
    "            distances[index] = distance\n",
    "            index+=1\n",
    "\n",
    "        return members[distances.argmin()]\n",
    "    \n",
    "    def fit(self, distance_matrix, radius, mask=None):\n",
    "        self.matrix_distances_=distance_matrix\n",
    "        self.n_samples=distance_matrix.shape[0]\n",
    "        self.labels_=np.array(range(0,self.n_samples))\n",
    "        \n",
    "        self.medoids_ = []\n",
    "        #here we use a mask to cache if the distance is less than radius,\n",
    "        #thus we don't have to compare them all them time.\n",
    "        if not mask:\n",
    "            mask = distance_matrix<radius\n",
    "            mask = mask * 1\n",
    "        while True:\n",
    "            mask_row_sum = mask.sum(axis=0)\n",
    "            if sum(mask_row_sum)<=1:\n",
    "                break\n",
    "            #candidate\n",
    "            old_medoid = mask_row_sum.argmax()\n",
    "            old_mask_row=mask[old_medoid]\n",
    "            if old_mask_row.shape[0]==2:\n",
    "                return\n",
    "            new_medoid=self.__select_medoid(old_mask_row)\n",
    "\n",
    "            new_mask_row=mask[new_medoid]\n",
    "\n",
    "            while (old_medoid!=new_medoid) \\\n",
    "                and (old_mask_row==new_mask_row).all() \\\n",
    "                and (sum(new_mask_row)>0):\n",
    "                old_medoid=new_medoid\n",
    "                old_mask_row=new_mask_row\n",
    "                new_medoid=self.__select_medoid(new_mask_row)\n",
    "                new_mask_row=mask[new_medoid]\n",
    "                if sum(new_mask_row)==0:\n",
    "                    break\n",
    "\n",
    "            members=new_mask_row.argsort()[-new_mask_row.sum():]\n",
    "            if len(members)>1:\n",
    "                self.medoids_.append(new_medoid)\n",
    "                self.labels_[members]=new_medoid\n",
    "            mask[members]=0\n",
    "            mask[:,members]=0"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [],
   "source": [
    "mr=MedoidRadius()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [],
   "source": [
    "mr.fit(distance_matrix, 0.15)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([0, 0, 2, 2, 4, 5])"
      ]
     },
     "execution_count": 22,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "mr.labels_"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
